#region Licensing

/*
 
 Microsoft Public License (Ms-PL)

This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software.

1. Definitions

The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.

A "contribution" is the original software, or any additions or changes to the software.

A "contributor" is any person that distributes its contribution under this license.

"Licensed patents" are a contributor's patent claims that read directly on its contribution.

2. Grant of Rights

(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.

(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.

3. Conditions and Limitations

(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.

(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.

(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.

(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.

(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. 
 
 */

#endregion

#region Using Statements

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

#endregion

namespace PortableJson
{
    /// <summary>
    /// This class encodes and decodes JSON strings.
    /// Spec. details, see http://www.json.org/
    /// This is the class that does the actual parsing of the JSON
    /// </summary>
    internal sealed class JsonParser
    {
        #region Enums

        /// <summary>
        /// Used to easily specify the different values that can be in a JSON string
        /// </summary>
        private enum Token
        {
            None = -1,           // Used to denote no Lookahead available
            Curly_Open,
            Curly_Close,
            Squared_Open,
            Squared_Close,
            Colon,
            Comma,
            String,
            Number,
            True,
            False,
            Null
        }

        #endregion

        #region Fields

        private readonly char[] jsonCharacterArray;
        private readonly bool ignoreCase;
        private Token lookAheadToken;
        private int jsonIndex;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="JsonParser"/> class.
        /// </summary>
        /// <param name="json">The JSON to parse.</param>
        /// <param name="ignoreCasing">if set to true the parsing will ignore casing when it comes to the name of JSON objects.</param>
        internal JsonParser(string json, bool ignoreCasing)
        {
            jsonCharacterArray = json.ToCharArray();
            ignoreCase = ignoreCasing;
            lookAheadToken = Token.None;
        }

        #endregion

        #region Exposed Members

        /// <summary>
        /// Decodes the JSON that was passed into the Constructor of the class
        /// </summary>
        /// <returns>A object that is usually in the form on List, Dictionary or KeyValuePair that holds the data of the previously given JSON</returns>
        public object Decode()
        {
            return ParseValue();
        }

        #endregion

        #region Private Members

        /// <summary>
        /// Parses a JSON object.
        /// </summary>
        /// <returns>A Dictionary of string,object that houses the information of the object.</returns>
        /// <exception cref="System.Exception">Expected colon at index.</exception>
        private Dictionary<string, object> ParseObject()
        {
            Dictionary<string, object> jsonObjectInformation = new Dictionary<string, object>();

            // Consume the opening { of the object
            ConsumeToken();

            while (true)
            {
                switch (LookAhead())
                {
                    // Consume the comma as we don't need it
                    case Token.Comma:
                        ConsumeToken();
                        break;

                    // Consume the token because it is the closing } of the object
                    // And return the Dictionary of string,object that contains the information
                    // of the JSON object being parsed
                    case Token.Curly_Close:
                        ConsumeToken();
                        return jsonObjectInformation;

                    default:
                        // Get the name of the JSON object
                        string name = ParseString();

                        if (ignoreCase)
                        {
                            name = name.ToLower();
                        }

                        // If the next token after the name is not a colon, throw an exception
                        if (NextToken() != Token.Colon)
                        {
                            throw new Exception("Expected colon at index " + jsonIndex);
                        }

                        // Get the value that is tied to this JSON object
                        jsonObjectInformation[name] = ParseValue();
               
                        break;
                }
            }
        }

        /// <summary>
        /// Parses an array of JSON objects.
        /// </summary>
        /// <returns>A List of JSON objects.</returns>
        private List<object> ParseArray()
        {
            List<object> jsonObjectsList = new List<object>();

            // Consume the beginning [ of the JSON array
            ConsumeToken();

            while (true)
            {
                switch (LookAhead())
                {
                    // Consume the comma as we don't need it
                    case Token.Comma:
                        ConsumeToken();
                        break;

                    // Consume the closing ] of the JSON array
                    // And then return the list of JSON objects
                    case Token.Squared_Close:
                        ConsumeToken();
                        return jsonObjectsList;

                    default:
                        jsonObjectsList.Add(ParseValue());
                        break;
                }
            }
        }

        /// <summary>
        /// Parses the value of a JSON object based on the next type of token in the JSON string.
        /// </summary>
        /// <returns>The object that contains the value of the current JSON object.</returns>
        /// <exception cref="System.Exception">Unrecognized token at index.</exception>
        private object ParseValue()
        {
            switch (LookAhead())
            {
                case Token.Number:
                    return ParseNumber();

                case Token.String:
                    return ParseString();

                case Token.Curly_Open:
                    return ParseObject();

                case Token.Squared_Open:
                    return ParseArray();

                case Token.True:
                    ConsumeToken();
                    return true;

                case Token.False:
                    ConsumeToken();
                    return false;

                case Token.Null:
                    ConsumeToken();
                    return null;
            }

            throw new Exception("Unrecognized token at index" + jsonIndex);
        }

        /// <summary>
        /// Parses the string from the given JSON string.
        /// </summary>
        /// <returns>The value of the current JSON object as a string object.</returns>
        /// <exception cref="System.Exception">Unexpectedly reached end of string.</exception>
        private string ParseString()
        {
            // Consume the opening " of the string
            ConsumeToken(); 

            var stringBuilder = new StringBuilder();
            int stringIndexInsideJson = -1;

            // While we don't go past the end of the given JSON string
            while (jsonIndex < jsonCharacterArray.Length)
            {
                // Get the current character from the JSON string
                var currentCharacter = jsonCharacterArray[jsonIndex++];

                if (currentCharacter == '"')
                {
                    // If we have found at least one other character of the string
                    if (stringIndexInsideJson != -1)
                    {
                        // If we have not yet appending to the string builder
                        if (stringBuilder.Length == 0)
                        {
                            return new string(jsonCharacterArray, stringIndexInsideJson, jsonIndex - stringIndexInsideJson - 1);
                        }

                        stringBuilder.Append(jsonCharacterArray, stringIndexInsideJson, jsonIndex - stringIndexInsideJson - 1);
                    }

                    return stringBuilder.ToString();
                }

                if (currentCharacter != '\\')
                {
                    if (stringIndexInsideJson == -1)
                    {
                        stringIndexInsideJson = jsonIndex - 1;
                    }

                    continue;
                }

                if (jsonIndex == jsonCharacterArray.Length)
                {
                    break;
                }

                if (stringIndexInsideJson != -1)
                {
                    stringBuilder.Append(jsonCharacterArray, stringIndexInsideJson, jsonIndex - stringIndexInsideJson - 1);
                    stringIndexInsideJson = -1;
                }

                // Append the corresponding character to the string builder based on the current index inside
                // of the JSON string
                switch (jsonCharacterArray[jsonIndex++])
                {
                    case '"':
                        stringBuilder.Append('"');
                        break;

                    case '\\':
                        stringBuilder.Append('\\');
                        break;

                    case '/':
                        stringBuilder.Append('/');
                        break;

                    case 'b':
                        stringBuilder.Append('\b');
                        break;

                    case 'f':
                        stringBuilder.Append('\f');
                        break;

                    case 'n':
                        stringBuilder.Append('\n');
                        break;

                    case 'r':
                        stringBuilder.Append('\r');
                        break;

                    case 't':
                        stringBuilder.Append('\t');
                        break;

                    case 'u':
                        {
                            int remainingLength = jsonCharacterArray.Length - jsonIndex;
                            if (remainingLength < 4) break;

                            // parse the 32 bit hex into an integer codepoint
                            uint codePoint = ParseUnicode(jsonCharacterArray[jsonIndex], jsonCharacterArray[jsonIndex + 1], jsonCharacterArray[jsonIndex + 2], jsonCharacterArray[jsonIndex + 3]);
                            stringBuilder.Append((char)codePoint);

                            // skip 4 chars
                            jsonIndex += 4;
                        }
                        break;
                }
            }

            throw new Exception("Unexpectedly reached end of string");
        }

        /// <summary>
        /// Takes in a character and returns the equivalent unsigned integer.
        /// </summary>
        /// <param name="characterToParse">The character to parse.</param>
        /// <param name="multiplier">The multiplier.</param>
        /// <returns>A unsigned integer that represents the given character.</returns>
        private uint ParseSingleChar(char characterToParse, uint multiplier)
        {
            uint p1 = 0;

            if (characterToParse >= '0' && characterToParse <= '9')
            {
                p1 = (uint)(characterToParse - '0') * multiplier;
            }
            else if (characterToParse >= 'A' && characterToParse <= 'F')
            {
                p1 = (uint)((characterToParse - 'A') + 10) * multiplier;
            }
            else if (characterToParse >= 'a' && characterToParse <= 'f')
            {
                p1 = (uint)((characterToParse - 'a') + 10) * multiplier;
            }

            return p1;
        }

        /// <summary>
        /// Parses the Unicode value from 4 given characters.
        /// </summary>
        /// <param name="character1">The first character to parse with.</param>
        /// <param name="character2">The second character to parse with.</param>
        /// <param name="character3">The third character to parse with.</param>
        /// <param name="character4">The fourth character to parse with.</param>
        /// <returns>An unsigned int that represents the Unicode value of the given characters</returns>
        private uint ParseUnicode(char character1, char character2, char character3, char character4)
        {
            uint p1 = ParseSingleChar(character1, 0x1000);
            uint p2 = ParseSingleChar(character2, 0x100);
            uint p3 = ParseSingleChar(character3, 0x10);
            uint p4 = ParseSingleChar(character4, 1);

            return p1 + p2 + p3 + p4;
        }

        /// <summary>
        /// Parses the number from the current location in the JSON string.
        /// </summary>
        /// <returns>Either a long or double value based on the number type.</returns>
        private object ParseNumber()
        {
            // Consume the next token in the JSON string
            ConsumeToken();

            // Need to start back one place because the first digit is also a token and would have been consumed
            var startIndex = jsonIndex - 1;
            bool isDoubleNumber = false;

            do
            {
                // Make sure we don't go past the length of given JSON
                if (jsonIndex == jsonCharacterArray.Length)
                {
                    break;
                }

                // Get the current character from the given JSON string
                var currentCharacter = jsonCharacterArray[jsonIndex];

                // If the current character is a token that can be associated with a number
                if ((currentCharacter >= '0' && currentCharacter <= '9') || currentCharacter == '.' || currentCharacter == '-' || currentCharacter == '+' || currentCharacter == 'e' || currentCharacter == 'E')
                {
                    // If the current character indicates the number being parsed should be treated like a double
                    if (currentCharacter == '.' || currentCharacter == 'e' || currentCharacter == 'E')
                    {
                        isDoubleNumber = true;
                    }

                    // If we have reached the end of the given JSON
                    if (++jsonIndex == jsonCharacterArray.Length)
                    {
                        break;
                    }

                    continue;
                }

                break;

            } while (true);

			if (isDoubleNumber)
			{
				string stringToParse = new string(jsonCharacterArray, startIndex, jsonIndex - startIndex);
				return double.Parse(stringToParse, NumberFormatInfo.InvariantInfo);
			}

			return CreateLong(jsonCharacterArray, startIndex, jsonIndex - startIndex);
        }

        /// <summary>
        /// Gets the next token in the JSON being looked at.
        /// </summary>
        /// <returns>The next token in the JSON being looked at.</returns>
        private Token LookAhead()
        {
            if (lookAheadToken != Token.None)
            {
                return lookAheadToken;
            }

            return lookAheadToken = NextTokenCore();
        }

        /// <summary>
        /// Sets up the look ahead token so it will consume the next token.
        /// </summary>
        private void ConsumeToken()
        {
            lookAheadToken = Token.None;
        }

        /// <summary>
        /// Gets the next token in the JSON being looked at.
        /// </summary>
        /// <returns>The next token in the JSON being looked at.</returns>
        private Token NextToken()
        {
            lookAheadToken = Token.None;

            return lookAheadToken != Token.None ? lookAheadToken : NextTokenCore(); ;
        }

        /// <summary>
        /// Gets the next token from the JSON being looked at by actually looking at the characters in the character array.
        /// </summary>
        /// <returns>The token that correlates to the character being looked at in the character array of the passed in JSON</returns>
        /// <exception cref="System.Exception">
        /// Reached end of string unexpectedly
        /// or
        /// Could not find token at index
        /// </exception>
        private Token NextTokenCore()
        {
            char currentCharacter;

            // Skip past the white space in the passed in JSON whitespace
            do
            {
                // Get the current characters
                currentCharacter = jsonCharacterArray[jsonIndex];

                // If we have a character that is greater than a space in the ASCII table
                if (currentCharacter > ' ')
                {
                    break;
                }

                // If the current character is not a special character
                if (currentCharacter != ' ' && currentCharacter != '\t' && currentCharacter != '\n' && currentCharacter != '\r')
                {
                    break;
                }

            } while (++jsonIndex < jsonCharacterArray.Length);

            // If we have reached the end of the passed in JSON string
            if (jsonIndex == jsonCharacterArray.Length)
            {
                throw new Exception("Reached end of string unexpectedly");
            }

            // Get the current character
            currentCharacter = jsonCharacterArray[jsonIndex];

            jsonIndex++;

            // Return the Token type that best corresponds with the current character(s)
            switch (currentCharacter)
            {
                case '{':
                    return Token.Curly_Open;

                case '}':
                    return Token.Curly_Close;

                case '[':
                    return Token.Squared_Open;

                case ']':
                    return Token.Squared_Close;

                case ',':
                    return Token.Comma;

                case '"':
                    return Token.String;

                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '-':
                case '+':
                case '.':
                    return Token.Number;

                case ':':
                    return Token.Colon;

                case 'f':
                    if (jsonCharacterArray.Length - jsonIndex >= 4 &&
                        jsonCharacterArray[jsonIndex + 0] == 'a' &&
                        jsonCharacterArray[jsonIndex + 1] == 'l' &&
                        jsonCharacterArray[jsonIndex + 2] == 's' &&
                        jsonCharacterArray[jsonIndex + 3] == 'e')
                    {
                        jsonIndex += 4;
                        return Token.False;
                    }
                    break;

                case 't':
                    if (jsonCharacterArray.Length - jsonIndex >= 3 &&
                        jsonCharacterArray[jsonIndex + 0] == 'r' &&
                        jsonCharacterArray[jsonIndex + 1] == 'u' &&
                        jsonCharacterArray[jsonIndex + 2] == 'e')
                    {
                        jsonIndex += 3;
                        return Token.True;
                    }
                    break;

                case 'n':
                    if (jsonCharacterArray.Length - jsonIndex >= 3 &&
                        jsonCharacterArray[jsonIndex + 0] == 'u' &&
                        jsonCharacterArray[jsonIndex + 1] == 'l' &&
                        jsonCharacterArray[jsonIndex + 2] == 'l')
                    {
                        jsonIndex += 3;
                        return Token.Null;
                    }
                    break;

            }

            throw new Exception("Could not find token at index " + --jsonIndex);
        }

        /// <summary>
        /// Creates a long from given characters along with where to start in the characters and the number of characters to use
        /// </summary>
        /// <param name="charactersContainingLong">The character array that contains the long the caller wants.</param>
        /// <param name="index">The index of where to start getting the long from the characters.</param>
        /// <param name="count">The count the number of characters the long is comprised of.</param>
        /// <returns>A long that is made from the passed in characters</returns>
        private long CreateLong(char[] charactersContainingLong, int index, int count)
        {
            long num = 0;
            bool isNegative = false;

            for (int x = 0; x < count; x++, index++)
            {
                char currentCharacter = charactersContainingLong[index];

                if (currentCharacter == '-')
                {
                    isNegative = true;
                }
                else if (currentCharacter == '+')
                {
                    isNegative = false;
                }
                else
                {
                    num *= 10;
                    num += (int)(currentCharacter - '0');
                }
            }

            if (isNegative)
            {
                num = -num;
            }

            return num;
        }

        #endregion
    }
}
